/**
 * Data format:
 *

--FormStreamBoundary1349886663601\r\n
Content-Disposition: form-data; name="foo"\r\n
\r\n
<FIELD-CONTENT>\r\n
--FormStreamBoundary1349886663601\r\n
Content-Disposition: form-data; name="file"; filename="formstream.test.js"\r\n
Content-Type: application/javascript\r\n
\r\n
<FILE-CONTENT>\r\n
--FormStreamBoundary1349886663601\r\n
Content-Disposition: form-data; name="pic"; filename="fawave.png"\r\n
Content-Type: image/png\r\n
\r\n
<IMAGE-CONTENT>\r\n
--FormStreamBoundary1349886663601--

 *
 */

import Stream from 'stream'
import util from 'util'
import path from 'path'
import fs from 'fs'
import destroy from 'destroy'
import mime from 'mine'
import parseStream from 'pause-stream'

var PADDING = '--';
var NEW_LINE = '\r\n';
var NEW_LINE_BUFFER = new Buffer(NEW_LINE);

function FormStream() {
    if (!(this instanceof FormStream)) {
        return new FormStream();
    }

    FormStream.super_.call(this);

    this._boundary = this._generateBoundary();
    this._streams = [];
    this._buffers = [];
    this._endData = new Buffer(PADDING + this._boundary + PADDING + NEW_LINE);
    this._contentLength = 0;
    this._isAllStreamSizeKnown = true;
    this._knownStreamSize = 0;
}

util.inherits(FormStream, Stream);
module.exports = FormStream;

FormStream.prototype._generateBoundary = function () {
    // https://github.com/felixge/node-form-data/blob/master/lib/form_data.js#L162
    // This generates a 50 character boundary similar to those used by Firefox.
    // They are optimized for boyer-moore parsing.
    var boundary = '--------------------------';
    for (var i = 0; i < 24; i++) {
        boundary += Math.floor(Math.random() * 10).toString(16);
    }

    return boundary;
};

FormStream.prototype.setTotalStreamSize = function (size) {
    // this method should not make any sense if the length of each stream is known.
    if (this._isAllStreamSizeKnown) {
        return this;
    }

    size = size || 0;

    for (var i = 0; i < this._streams.length; i++) {
        size += this._streams[i][0].length;
        size += NEW_LINE_BUFFER.length; // stream field end pedding size
    }

    this._knownStreamSize = size;
    this._isAllStreamSizeKnown = true;

    return this;
};

FormStream.prototype.headers = function (options) {
    var headers = {
        'Content-Type': 'multipart/form-data; boundary=' + this._boundary
    };

    // calculate total stream size
    this._contentLength += this._knownStreamSize;

    // calculate length of end padding
    this._contentLength += this._endData.length;

    if (this._isAllStreamSizeKnown) {
        headers['Content-Length'] = String(this._contentLength);
    }

    if (options) {
        for (var k in options) {
            headers[k] = options[k];
        }
    }

    return headers;
};

FormStream.prototype.file = function (name, filepath, filename, filesize) {
    var mimeType = mime.lookup(filepath);

    if (typeof filename === 'number' && !filesize) {
        filesize = filename;
        filename = path.basename(filepath);
    } else if (!filename) {
        filename = path.basename(filepath);
    }

    var stream = fs.createReadStream(filepath);

    return this.stream(name, stream, filename, mimeType, filesize);
};

/**
 * Add a form field
 * @param  {String} name field name
 * @param  {String|Buffer} value field value
 * @return {this}
 */
FormStream.prototype.field = function (name, value) {
    if (!Buffer.isBuffer(value)) {
        // field(String, Number)
        // https://github.com/qiniu/nodejs-sdk/issues/123
        if (typeof value === 'number') {
            value = String(value);
        }
        value = new Buffer(value);
    }
    return this.buffer(name, value);
};

FormStream.prototype.stream = function (name, stream, filename, mimeType, size) {
    if (typeof mimeType === 'number' && !size) {
        size = mimeType;
        mimeType = mime.lookup(filename);
    } else if (!mimeType) {
        mimeType = mime.lookup(filename);
    }

    stream.once('error', this.emit.bind(this, 'error'));
    // if form stream destroy, also destroy the source stream
    this.once('destroy', function () {
        destroy(stream);
    });

    var leading = this._leading({ name: name, filename: filename }, mimeType);

    var ps = parseStream().pause();
    stream.pipe(ps);

    this._streams.push([leading, ps]);

    // if the size of this stream is known, plus the total content-length;
    // otherwise, content-length is unknown.
    if (typeof size === 'number') {
        this._knownStreamSize += leading.length;
        this._knownStreamSize += size;
        this._knownStreamSize += NEW_LINE_BUFFER.length;
    } else {
        this._isAllStreamSizeKnown = false;
    }

    process.nextTick(this.resume.bind(this));

    return this;
};

FormStream.prototype.buffer = function (name, buffer, filename, mimeType) {
    if (filename && !mimeType) {
        mimeType = mime.lookup(filename);
    }

    var disposition = { name: name };
    if (filename) {
        disposition.filename = filename;
    }

    var leading = this._leading(disposition, mimeType);

    this._buffers.push([leading, buffer]);

    // plus buffer length to total content-length
    this._contentLength += leading.length;
    this._contentLength += buffer.length;
    this._contentLength += NEW_LINE_BUFFER.length;

    process.nextTick(this.resume.bind(this));

    return this;
};

FormStream.prototype._leading = function (disposition, type) {
    var leading = [PADDING + this._boundary];

    var disps = [];

    if (disposition) {
        for (var k in disposition) {
            disps.push(k + '="' + disposition[k] + '"');
        }
    }

    leading.push('Content-Disposition: form-data; ' + disps.join('; '));

    if (type) {
        leading.push('Content-Type: ' + type);
    }

    leading.push('');
    leading.push('');

    return new Buffer(leading.join(NEW_LINE));
};

FormStream.prototype._emitBuffers = function () {
    if (!this._buffers.length) {
        return;
    }

    for (var i = 0; i < this._buffers.length; i++) {
        var item = this._buffers[i];
        this.emit('data', item[0]); // part leading
        this.emit('data', item[1]); // part content
        this.emit('data', NEW_LINE_BUFFER);
    }

    this._buffers = [];
};

FormStream.prototype._emitStream = function (item) {
    var self = this;
    // item: [ fieldData, stream ]
    self.emit('data', item[0]);

    var stream = item[1];
    stream.on('data', function (data) {
        self.emit('data', data);
    });
    stream.on('end', function () {
        self.emit('data', NEW_LINE_BUFFER);
        return process.nextTick(self.drain.bind(self));
    });
    stream.resume();
};

FormStream.prototype._emitEnd = function () {
    // ending format:
    //
    // --{boundary}--\r\n
    this.emit('data', this._endData);
    this.emit('end');
};

FormStream.prototype.drain = function () {
    this._emitBuffers();

    var item = this._streams.shift();
    if (item) {
        this._emitStream(item);
    } else {
        this._emitEnd();
    }

    return this;
};

FormStream.prototype.resume = function () {
    this.paused = false;

    if (!this._draining) {
        this._draining = true;
        this.drain();
    }

    return this;
};

FormStream.prototype.close = FormStream.prototype.destroy = function () {
    this.emit('destroy');
};
